﻿#region

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

#endregion

namespace Toolbox.Dynamic
{
    public interface IProxyHandler
    {
        object Call(MethodInfo method, object[] args);
    }

    public class DynamicProxy<IProtocol> where IProtocol : class
    {
        private IProxyHandler _handler;
        private IProtocol _proxy;

        protected DynamicProxy(IProxyHandler handler)
        {
            _handler = Contract.NotNull(handler, "handler");
            _proxy = Activator.CreateInstance(_proxyType, handler, _methods) as IProtocol;
        }

        public IProtocol Proxy
        {
            get { return _proxy; }
        }

        public IProxyHandler Handler
        {
            get { return _handler; }
        }

        public static Type ProtocolType
        {
            get { return typeof (IProtocol); }
        }

        public static IProtocol Create(IProxyHandler handler)
        {
            return (new DynamicProxy<IProtocol>(handler)).Proxy;
        }

        #region Dynamic Type Creation

        private static MethodInfo[] _methods;
        private static Type _proxyType;

        private static IDictionary<string, DynamicAssembly> assemblyCache =
            new Dictionary<string, DynamicAssembly>();

        static DynamicProxy()
        {
            CreateDynamicProxyClass();
        }

        private static IDynamicAssembly GetDynamicProxyAssembly(string name)
        {
            if (!assemblyCache.ContainsKey(name))
                assemblyCache[name] = new DynamicAssembly(name);
            return assemblyCache[name];
        }

        private static void CreateDynamicProxyClass()
        {
            string assemblyName = ProtocolType.Namespace + ".DynamicProxies";
            string proxyTypeName = assemblyName + "." + ProtocolType.Name;

            Type parentType = null;
            Type[] interfaces = null;

            if (ProtocolType.IsInterface)
                interfaces = new[] {ProtocolType};
            else
                parentType = ProtocolType;

            IDynamicAssembly assembly = GetDynamicProxyAssembly(assemblyName);
            IDynamicClass c = assembly.DefineDynamicClass(proxyTypeName, parentType, interfaces);

            FieldBuilder handlerField = c.AddField("_handler", typeof (IProxyHandler));
            FieldBuilder methodsField = c.AddField("_methods", typeof (MethodInfo[]));

            IDynamicMethodBody ctorBody = new ConstructorMethodBody(handlerField, methodsField);
            c.AddCtor(ctorBody, typeof (IProxyHandler), typeof (MethodInfo[]));

            MethodInfo handlerMethod = typeof (IProxyHandler).GetMethod("Call");

            _methods = ProtocolType.GetMethods();

            for (int methodId = 0; methodId < _methods.Length; methodId++) {
                IDynamicMethodBody mBody = new ProxyMethodBody(
                    handlerField, methodsField, handlerMethod, _methods[methodId], methodId);
                c.AddMethod(mBody, _methods[methodId]);
            }

            _proxyType = c.CreateType();

            //assembly.Save();
        }

        #region Nested type: ConstructorMethodBody

        private class ConstructorMethodBody : IDynamicMethodBody
        {
            private FieldBuilder _handlerField;
            private FieldBuilder _methodsField;

            public ConstructorMethodBody(FieldBuilder handlerField, FieldBuilder methodsField)
            {
                _handlerField = Contract.NotNull(handlerField, "handlerField");
                _methodsField = Contract.NotNull(methodsField, "methodsField");
            }

            #region IDynamicMethodBody Members

            public void Emit(ILGenerator ilg)
            {
                Contract.NotNull(ilg, "ilg");

                ilg.Emit(OpCodes.Ldarg_0);
                ilg.Emit(OpCodes.Ldarg_1);
                ilg.Emit(OpCodes.Stfld, _handlerField);

                ilg.Emit(OpCodes.Ldarg_0);
                ilg.Emit(OpCodes.Ldarg_2);
                ilg.Emit(OpCodes.Stfld, _methodsField);

                ilg.Emit(OpCodes.Ret);
            }

            #endregion
        }

        #endregion

        #region Nested type: ProxyMethodBody

        private class ProxyMethodBody : IDynamicMethodBody
        {
            private static readonly Dictionary<Type, OpCode> LdPrimitive =
                new Dictionary<Type, OpCode>();

            private FieldBuilder _handlerField;
            private MethodInfo _handlerMethod;
            private MethodInfo _method;
            private int _methodId;
            private FieldBuilder _methodsField;

            static ProxyMethodBody()
            {
                LdPrimitive.Add(typeof (Boolean), OpCodes.Ldind_I1);
                LdPrimitive.Add(typeof (Int16), OpCodes.Ldind_I2);
                LdPrimitive.Add(typeof (Int32), OpCodes.Ldind_I4);
                LdPrimitive.Add(typeof (Int64), OpCodes.Ldind_I8);
                LdPrimitive.Add(typeof (Double), OpCodes.Ldind_R8);
                LdPrimitive.Add(typeof (Single), OpCodes.Ldind_R4);
                LdPrimitive.Add(typeof (UInt16), OpCodes.Ldind_U2);
                LdPrimitive.Add(typeof (UInt32), OpCodes.Ldind_U4);
            }

            public ProxyMethodBody(
                FieldBuilder handlerField, FieldBuilder methodsField, MethodInfo handlerMethod,
                MethodInfo method, int methodId)
            {
                _handlerField = Contract.NotNull(handlerField, "handlerField");
                _methodsField = Contract.NotNull(methodsField, "methodsField");
                _handlerMethod = Contract.NotNull(handlerMethod, "handlerMethod");
                _method = Contract.NotNull(method, "method");
                _methodId = Contract.IsAtLeast(methodId, 0, "methodId");
            }

            #region IDynamicMethodBody Members

            public void Emit(ILGenerator ilg)
            {
                Contract.NotNull(ilg, "ilg");

                LocalBuilder args = ilg.DeclareLocal(typeof (object[]));

                ilg.Emit(OpCodes.Ldc_I4, _method.GetParameters().Length);
                ilg.Emit(OpCodes.Newarr, typeof (object));
                ilg.Emit(OpCodes.Stloc, args);

                int i = 1;
                foreach (ParameterInfo p in _method.GetParameters()) {
                    ilg.Emit(OpCodes.Ldloc, args);
                    ilg.Emit(OpCodes.Ldc_I4, i - 1);
                    ilg.Emit(OpCodes.Ldarg, i);
                    if (p.ParameterType.IsValueType)
                        ilg.Emit(OpCodes.Box, p.ParameterType);
                    ilg.Emit(OpCodes.Stelem_Ref);
                    i++;
                }

                ilg.Emit(OpCodes.Ldarg_0);
                ilg.Emit(OpCodes.Ldfld, _handlerField);

                ilg.Emit(OpCodes.Ldarg_0);
                ilg.Emit(OpCodes.Ldfld, _methodsField);
                ilg.Emit(OpCodes.Ldc_I4, _methodId);
                ilg.Emit(OpCodes.Ldelem_Ref);

                ilg.Emit(OpCodes.Ldloc, args);

                // Call the handler method and return the result
                ilg.Emit(OpCodes.Callvirt, _handlerMethod);

                if (_method.ReturnType == typeof (void))
                    ilg.Emit(OpCodes.Pop);
                else if (_method.ReturnType.IsValueType) {
                    ilg.Emit(OpCodes.Unbox, _method.ReturnType);
                    if (_method.ReturnType.IsEnum)
                        ilg.Emit(OpCodes.Ldind_I4);
                    else if (!_method.ReturnType.IsPrimitive)
                        ilg.Emit(OpCodes.Ldobj, _method.ReturnType);
                    else
                        ilg.Emit(LdPrimitive[_method.ReturnType]);
                }

                ilg.Emit(OpCodes.Ret);
            }

            #endregion
        }

        #endregion

        #endregion
    }
}